/**
 * Copyright (c) 2001-2012 Steve Purcell.
 * Copyright (c) 2002      Vidar Holen.
 * Copyright (c) 2002      Michal Ceresna.
 * Copyright (c) 2005      Ewan Mellor.
 * Copyright (c) 2010-2012 penSec.IT UG (haftungsbeschr?nkt).
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer. Redistributions in binary
 * form must reproduce the above copyright notice, this list of conditions and
 * the following disclaimer in the documentation and/or other materials provided
 * with the distribution. Neither the name of the copyright holder nor the names
 * of its contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package ssq.utils;

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * Largely GNU-compatible command-line options parser. Has short (-v) and long-form (--verbose) option support, and also allows options with associated values (-d 2, --debug 2, --debug=2). Option processing can be explicitly terminated by the argument '--'.
 *
 * @author Steve Purcell
 * @author penSec.IT UG (haftungsbeschr?nkt)
 *
 * @version 2.0
 * @see com.sanityinc.jargs.examples.OptionTest
 */
public class CmdLineParser
{
    
    /**
     * Base class for exceptions that may be thrown when options are parsed
     */
    public static abstract class OptionException extends Exception
    {
        /**
         *
         */
        private static final long serialVersionUID = -2723431225728284622L;
        
        OptionException(String msg)
        {
            super(msg);
        }
    }
    
    /**
     * Thrown when the parsed command-line contains an option that is not recognised. <code>getMessage()</code> returns an error string suitable for reporting the error to the user (in English).
     */
    public static class UnknownOptionException extends OptionException
    {
        /**
         *
         */
        private static final long serialVersionUID = 4120924448815290572L;
        
        UnknownOptionException(String optionName)
        {
            this(optionName, "Unknown option '" + optionName + "'");
        }
        
        UnknownOptionException(String optionName, String msg)
        {
            super(msg);
            this.optionName = optionName;
        }
        
        /**
         * @return the name of the option that was unknown (e.g. "-u")
         */
        public String getOptionName()
        {
            return this.optionName;
        }
        
        private final String optionName;
    }
    
    /**
     * Thrown when the parsed commandline contains multiple concatenated short options, such as -abcd, where one is unknown. <code>getMessage()</code> returns an english human-readable error string.
     *
     * @author Vidar Holen
     */
    public static class UnknownSuboptionException
            extends UnknownOptionException
    {
        /**
         *
         */
        private static final long serialVersionUID = -3345770910673146561L;
        private char              suboption;
        
        UnknownSuboptionException(String option, char suboption)
        {
            super(option, "Illegal option: '" + suboption + "' in '" + option + "'");
            this.suboption = suboption;
        }
        
        public char getSuboption()
        {
            return suboption;
        }
    }
    
    /**
     * Thrown when the parsed commandline contains multiple concatenated short options, such as -abcd, where one or more requires a value. <code>getMessage()</code> returns an english human-readable error string.
     *
     * @author Vidar Holen
     */
    public static class NotFlagException extends UnknownOptionException
    {
        /**
         *
         */
        private static final long serialVersionUID = -2272820739273914892L;
        private char              notflag;
        
        NotFlagException(String option, char unflaggish)
        {
            super(option, "Illegal option: '" + option + "', '" +
                    unflaggish + "' requires a value");
            notflag = unflaggish;
        }
        
        /**
         * @return the first character which wasn't a boolean (e.g 'c')
         */
        public char getOptionChar()
        {
            return notflag;
        }
    }
    
    /**
     * Thrown when an illegal or missing value is given by the user for an option that takes a value. <code>getMessage()</code> returns an error string suitable for reporting the error to the user (in English).
     *
     * No generic class can ever extend <code>java.lang.Throwable</code>, so we have to return <code>Option&lt;?&gt;</code> instead of <code>Option&lt;T&gt;</code>.
     */
    public static class IllegalOptionValueException extends OptionException
    {
        /**
         *
         */
        private static final long serialVersionUID = 3406511253739568710L;
        
        public <T> IllegalOptionValueException(Option<T> opt, String value)
        {
            super("Illegal value '" + value + "' for option " +
                    (opt.shortForm() != null ? "-" + opt.shortForm() + "/" : "") +
                    "--" + opt.longForm());
            this.option = opt;
            this.value = value;
        }
        
        /**
         * @return the name of the option whose value was illegal (e.g. "-u")
         */
        public Option<?> getOption()
        {
            return this.option;
        }
        
        /**
         * @return the illegal value
         */
        public String getValue()
        {
            return this.value;
        }
        
        private final Option<?> option;
        private final String    value;
    }
    
    /**
     * Representation of a command-line option
     *
     * @param T
     *            Type of data configured by this option
     */
    public static abstract class Option<T>
    {
        
        protected Option(String longForm, boolean wantsValue)
        {
            this(null, longForm, wantsValue);
        }
        
        protected Option(char shortForm, String longForm,
                boolean wantsValue)
        {
            this(new String(new char[] { shortForm }), longForm, wantsValue);
        }
        
        private Option(String shortForm, String longForm, boolean wantsValue)
        {
            if (longForm == null)
            {
                throw new IllegalArgumentException("Null longForm not allowed");
            }
            this.shortForm = shortForm;
            this.longForm = longForm;
            this.wantsValue = wantsValue;
        }
        
        public String shortForm()
        {
            return this.shortForm;
        }
        
        public String longForm()
        {
            return this.longForm;
        }
        
        /**
         * Tells whether or not this option wants a value
         */
        public boolean wantsValue()
        {
            return this.wantsValue;
        }
        
        public final T getValue(String arg, Locale locale)
                throws IllegalOptionValueException
        {
            if (this.wantsValue)
            {
                if (arg == null)
                {
                    throw new IllegalOptionValueException(this, "");
                }
                return this.parseValue(arg, locale);
            }
            else
            {
                return this.getDefaultValue();
            }
        }
        
        /**
         * Override to extract and convert an option value passed on the command-line
         */
        protected T parseValue(String arg, Locale locale)
                throws IllegalOptionValueException
        {
            
            return null;
        }
        
        /**
         * Override to define default value returned by getValue if option does not want a value
         */
        protected T getDefaultValue()
        {
            return null;
        }
        
        private final String  shortForm;
        private final String  longForm;
        private final boolean wantsValue;
        
        /**
         * An option that expects a boolean value
         */
        public static class BooleanOption extends Option<Boolean>
        {
            public BooleanOption(char shortForm, String longForm)
            {
                super(shortForm, longForm, false);
            }
            
            public BooleanOption(String longForm)
            {
                super(longForm, false);
            }
            
            @Override
            public Boolean parseValue(String arg, Locale lcoale)
            {
                return Boolean.TRUE;
            }
            
            @Override
            public Boolean getDefaultValue()
            {
                return Boolean.TRUE;
            }
        }
        
        /**
         * An option that expects an integer value
         */
        public static class IntegerOption extends Option<Integer>
        {
            public IntegerOption(char shortForm, String longForm)
            {
                super(shortForm, longForm, true);
            }
            
            public IntegerOption(String longForm)
            {
                super(longForm, true);
            }
            
            @Override
            protected Integer parseValue(String arg, Locale locale)
                    throws IllegalOptionValueException
            {
                try
                {
                    return new Integer(arg);
                }
                catch (NumberFormatException e)
                {
                    throw new IllegalOptionValueException(this, arg);
                }
            }
        }
        
        /**
         * An option that expects a long integer value
         */
        public static class LongOption extends Option<Long>
        {
            public LongOption(char shortForm, String longForm)
            {
                super(shortForm, longForm, true);
            }
            
            public LongOption(String longForm)
            {
                super(longForm, true);
            }
            
            @Override
            protected Long parseValue(String arg, Locale locale)
                    throws IllegalOptionValueException
            {
                try
                {
                    return new Long(arg);
                }
                catch (NumberFormatException e)
                {
                    throw new IllegalOptionValueException(this, arg);
                }
            }
        }
        
        /**
         * An option that expects a floating-point value
         */
        public static class DoubleOption extends Option<Double>
        {
            public DoubleOption(char shortForm, String longForm)
            {
                super(shortForm, longForm, true);
            }
            
            public DoubleOption(String longForm)
            {
                super(longForm, true);
            }
            
            @Override
            protected Double parseValue(String arg, Locale locale)
                    throws IllegalOptionValueException
            {
                try
                {
                    NumberFormat format = NumberFormat.getNumberInstance(locale);
                    Number num = format.parse(arg);
                    return new Double(num.doubleValue());
                }
                catch (ParseException e)
                {
                    throw new IllegalOptionValueException(this, arg);
                }
            }
        }
        
        /**
         * An option that expects a string value
         */
        public static class StringOption extends Option<String>
        {
            public StringOption(char shortForm, String longForm)
            {
                super(shortForm, longForm, true);
            }
            
            public StringOption(String longForm)
            {
                super(longForm, true);
            }
            
            @Override
            protected String parseValue(String arg, Locale locale)
            {
                return arg;
            }
        }
    }
    
    /**
     * Add the specified Option to the list of accepted options
     */
    public final <T> Option<T> addOption(Option<T> opt)
    {
        if (opt.shortForm() != null)
        {
            this.options.put("-" + opt.shortForm(), opt);
        }
        this.options.put("--" + opt.longForm(), opt);
        return opt;
    }
    
    /**
     * Convenience method for adding a string option.
     *
     * @return the new Option
     */
    public final Option<String> addStringOption(char shortForm, String longForm)
    {
        return addOption(new Option.StringOption(shortForm, longForm));
    }
    
    /**
     * Convenience method for adding a string option.
     *
     * @return the new Option
     */
    public final Option<String> addStringOption(String longForm)
    {
        return addOption(new Option.StringOption(longForm));
    }
    
    /**
     * Convenience method for adding an integer option.
     *
     * @return the new Option
     */
    public final Option<Integer> addIntegerOption(char shortForm, String longForm)
    {
        return addOption(new Option.IntegerOption(shortForm, longForm));
    }
    
    /**
     * Convenience method for adding an integer option.
     *
     * @return the new Option
     */
    public final Option<Integer> addIntegerOption(String longForm)
    {
        return addOption(new Option.IntegerOption(longForm));
    }
    
    /**
     * Convenience method for adding a long integer option.
     *
     * @return the new Option
     */
    public final Option<Long> addLongOption(char shortForm, String longForm)
    {
        return addOption(new Option.LongOption(shortForm, longForm));
    }
    
    /**
     * Convenience method for adding a long integer option.
     *
     * @return the new Option
     */
    public final Option<Long> addLongOption(String longForm)
    {
        return addOption(new Option.LongOption(longForm));
    }
    
    /**
     * Convenience method for adding a double option.
     *
     * @return the new Option
     */
    public final Option<Double> addDoubleOption(char shortForm, String longForm)
    {
        return addOption(new Option.DoubleOption(shortForm, longForm));
    }
    
    /**
     * Convenience method for adding a double option.
     *
     * @return the new Option
     */
    public final Option<Double> addDoubleOption(String longForm)
    {
        return addOption(new Option.DoubleOption(longForm));
    }
    
    /**
     * Convenience method for adding a boolean option.
     *
     * @return the new Option
     */
    public final Option<Boolean> addBooleanOption(char shortForm, String longForm)
    {
        return addOption(new Option.BooleanOption(shortForm, longForm));
    }
    
    /**
     * Convenience method for adding a boolean option.
     *
     * @return the new Option
     */
    public final Option<Boolean> addBooleanOption(String longForm)
    {
        return addOption(new Option.BooleanOption(longForm));
    }
    
    /**
     * Equivalent to {@link #getOptionValue(Option, Object) getOptionValue(o, null)}.
     */
    public <T> T getOptionValue(Option<T> o)
    {
        return getOptionValue(o, null);
    }
    
    /**
     * @return the parsed value of the given Option, or the given default 'def' if the option was not set
     */
    public <T> T getOptionValue(Option<T> o, T def)
    {
        List<?> v = values.get(o.longForm());
        
        if (v == null)
        {
            return def;
        }
        else if (v.isEmpty())
        {
            return null;
        }
        else
        {
            
            /* Cast should be safe because Option.parseValue has to return an
             * instance of type T or null
             */
            @SuppressWarnings("unchecked")
            T result = (T) v.remove(0);
            return result;
        }
    }
    
    /**
     * @return A Collection giving the parsed values of all the occurrences of the given Option, or an empty Collection if the option was not set.
     */
    public final <T> Collection<T> getOptionValues(Option<T> option)
    {
        Collection<T> result = new ArrayList<T>();
        
        while (true)
        {
            T o = getOptionValue(option, null);
            
            if (o == null)
            {
                return result;
            }
            else
            {
                result.add(o);
            }
        }
    }
    
    /**
     * @return the non-option arguments
     */
    public final String[] getRemainingArgs()
    {
        return this.remainingArgs;
    }
    
    /**
     * Extract the options and non-option arguments from the given list of command-line arguments. The default locale is used for parsing options whose values might be locale-specific.
     */
    public void parse(String[] argv) throws OptionException
    {
        parse(argv, Locale.getDefault());
    }
    
    /**
     * Extract the options and non-option arguments from the given list of command-line arguments. The specified locale is used for parsing options whose values might be locale-specific.
     */
    public final void parse(String[] argv, Locale locale)
            throws OptionException
    {
        
        ArrayList<Object> otherArgs = new ArrayList<Object>();
        int position = 0;
        this.values = new HashMap<String, List<?>>(10);
        while (position < argv.length)
        {
            String curArg = argv[position];
            if (curArg.startsWith("-"))
            {
                if (curArg.equals("--"))
                { // end of options
                    position += 1;
                    break;
                }
                String valueArg = null;
                if (curArg.startsWith("--"))
                { // handle --arg=value
                    int equalsPos = curArg.indexOf("=");
                    if (equalsPos != -1)
                    {
                        valueArg = curArg.substring(equalsPos + 1);
                        curArg = curArg.substring(0, equalsPos);
                    }
                }
                else if (curArg.length() > 2)
                { // handle -abcd
                    for (int i = 1; i < curArg.length(); i++)
                    {
                        Option<?> opt = this.options.get("-" + curArg.charAt(i));
                        if (opt == null)
                        {
                            throw new UnknownSuboptionException(curArg, curArg.charAt(i));
                        }
                        if (opt.wantsValue())
                        {
                            throw new NotFlagException(curArg, curArg.charAt(i));
                        }
                        addValue(opt, null, locale);
                        
                    }
                    position++;
                    continue;
                }
                
                Option<?> opt = this.options.get(curArg);
                if (opt == null)
                {
                    throw new UnknownOptionException(curArg);
                }
                
                if (opt.wantsValue())
                {
                    if (valueArg == null)
                    {
                        position += 1;
                        if (position < argv.length)
                        {
                            valueArg = argv[position];
                        }
                    }
                    addValue(opt, valueArg, locale);
                }
                else
                {
                    addValue(opt, null, locale);
                }
                
                position += 1;
            }
            else
            {
                otherArgs.add(curArg);
                position += 1;
            }
        }
        for (; position < argv.length; ++position)
        {
            otherArgs.add(argv[position]);
        }
        
        this.remainingArgs = new String[otherArgs.size()];
        remainingArgs = otherArgs.toArray(remainingArgs);
    }
    
    private <T> void addValue(Option<T> opt, String valueArg, Locale locale)
            throws IllegalOptionValueException
    {
        
        T value = opt.getValue(valueArg, locale);
        String lf = opt.longForm();
        
        /* Cast is typesafe because the only location we add elements to the
         * values map is in this method.
         */
        @SuppressWarnings("unchecked")
        List<T> v = (List<T>) values.get(lf);
        
        if (v == null)
        {
            v = new ArrayList<T>();
            values.put(lf, v);
        }
        
        v.add(value);
    }
    
    private String[]               remainingArgs = null;
    private Map<String, Option<?>> options       = new HashMap<String, Option<?>>(10);
    private Map<String, List<?>>   values        = new HashMap<String, List<?>>(10);
}
